/**********************************************************************

  Audacity: A Digital Audio Editor

  MeterPanel.cpp

  Dominic Mazzoni
  Vaughan Johnson

  2004.06.25 refresh rate limited to 30mS, by ChackoN

*******************************************************************//**

\class MeterPanel
\brief VU Meter, for displaying recording/playback level

  This is a bunch of common code that can display many different
  forms of VU meters and other displays.

  But note that a lot of later code here assumes these are
  MeterToolBar meters, e.g., MeterPanel::StartMonitoring,
  so these are not as generic/common as originally intended.

*//****************************************************************//**

\class MeterBar
\brief A struct used by MeterPanel to hold the position of one bar.

*//****************************************************************//**

\class MeterUpdateMsg
\brief Message used to update the MeterPanel

*//****************************************************************//**

\class MeterUpdateQueue
\brief Queue of MeterUpdateMsg used to feed the MeterPanel.

*//******************************************************************/

#include "MeterPanel.h"

#include <algorithm>
#include <wx/setup.h> // for wxUSE_* macros
#include <wx/wxcrtvararg.h>
#include <wx/defs.h>
#include <wx/dcbuffer.h>
#include <wx/frame.h>
#include <wx/menu.h>
#include <wx/settings.h>
#include <wx/textdlg.h>
#include <wx/numdlg.h>
#include <wx/radiobut.h>
#include <wx/tooltip.h>

#include <math.h>

#include "AudioIO.h"
#include "AColor.h"
#include "../widgets/BasicMenu.h"
#include "ImageManipulation.h"
#include "Decibels.h"
#include "LinearUpdater.h"
#include "Project.h"
#include "ProjectAudioIO.h"
#include "ProjectStatus.h"
#include "../ProjectWindows.h"
#include "Prefs.h"
#include "ShuttleGui.h"
#include "Theme.h"
#include "wxWidgetsWindowPlacement.h"
#include "../widgets/LinearUpdater.h"
#include "../widgets/LinearDBFormat.h"
#include "../widgets/RealFormat.h"

#include "AllThemeResources.h"
#include "../widgets/valnum.h"

#if wxUSE_ACCESSIBILITY
#include "WindowAccessible.h"

class MeterAx final : public WindowAccessible
{
public:
    MeterAx(wxWindow* window);

    virtual ~MeterAx();

    // Performs the default action. childId is 0 (the action for this object)
    // or > 0 (the action for a child).
    // Return wxACC_NOT_SUPPORTED if there is no default action for this
    // window (e.g. an edit control).
    wxAccStatus DoDefaultAction(int childId) override;

    // Retrieves the address of an IDispatch interface for the specified child.
    // All objects must support this property.
    wxAccStatus GetChild(int childId, wxAccessible** child) override;

    // Gets the number of children.
    wxAccStatus GetChildCount(int* childCount) override;

    // Gets the default action for this object (0) or > 0 (the action for a child).
    // Return wxACC_OK even if there is no action. actionName is the action, or the empty
    // string if there is no action.
    // The retrieved string describes the action that is performed on an object,
    // not what the object does as a result. For example, a toolbar button that prints
    // a document has a default action of "Press" rather than "Prints the current document."
    wxAccStatus GetDefaultAction(int childId, wxString* actionName) override;

    // Returns the description for this object or a child.
    wxAccStatus GetDescription(int childId, wxString* description) override;

    // Gets the window with the keyboard focus.
    // If childId is 0 and child is NULL, no object in
    // this subhierarchy has the focus.
    // If this object has the focus, child should be 'this'.
    wxAccStatus GetFocus(int* childId, wxAccessible** child) override;

    // Returns help text for this object or a child, similar to tooltip text.
    wxAccStatus GetHelpText(int childId, wxString* helpText) override;

    // Returns the keyboard shortcut for this object or child.
    // Return e.g. ALT+K
    wxAccStatus GetKeyboardShortcut(int childId, wxString* shortcut) override;

    // Returns the rectangle for this object (id = 0) or a child element (id > 0).
    // rect is in screen coordinates.
    wxAccStatus GetLocation(wxRect& rect, int elementId) override;

    // Returns a role constant.
    wxAccStatus GetRole(int childId, wxAccRole* role) override;

    // Gets a variant representing the selected children
    // of this object.
    // Acceptable values:
    // - a null variant (IsNull() returns TRUE)
    // - a list variant (GetType() == wxT("list"))
    // - an integer representing the selected child element,
    //   or 0 if this object is selected (GetType() == wxT("long"))
    // - a "void*" pointer to a wxAccessible child object
    wxAccStatus GetSelections(wxVariant* selections) override;

    // Returns a state constant.
    wxAccStatus GetState(int childId, long* state) override;

    // Returns a localized string representing the value for the object
    // or child.
    wxAccStatus GetValue(int childId, wxString* strValue) override;
};

#endif // wxUSE_ACCESSIBILITY

static const long MIN_REFRESH_RATE = 1;
static const long MAX_REFRESH_RATE = 100;

/* Updates to the meter are passed across via meter updates, each contained in
 * a MeterUpdateMsg object */
wxString MeterUpdateMsg::toString()
{
    wxString output; // somewhere to build up a string in
    output = wxString::Format(wxT("Meter update msg: %i channels, %i samples\n"), \
                              kMaxMeterBars, numFrames);
    for (int i = 0; i < kMaxMeterBars; i++) { // for each channel of the meters
        output += wxString::Format(wxT("%f peak, %f rms "), peak[i], rms[i]);
        if (clipping[i]) {
            output += wxString::Format(wxT("clipped "));
        } else {
            output += wxString::Format(wxT("no clip "));
        }
        output += wxString::Format(wxT("%i head, %i tail\n"), headPeakCount[i], tailPeakCount[i]);
    }
    return output;
}

wxString MeterUpdateMsg::toStringIfClipped()
{
    for (int i = 0; i < kMaxMeterBars; i++) {
        if (clipping[i] || (headPeakCount[i] > 0) || (tailPeakCount[i] > 0)) {
            return toString();
        }
    }
    return wxT("");
}

//
// MeterPanel class
//

#include "../../images/SpeakerMenu.xpm"
#include "../../images/MicMenu.xpm"

// How many pixels between items?
const static int gap = 0;

const static wxChar* PrefStyles[] =
{
    wxT("AutomaticStereo"),
    wxT("HorizontalStereo"),
    wxT("VerticalStereo")
};

enum {
    OnMeterUpdateID = 6000,
    OnMonitorID,
    OnPreferencesID,
    OnTipTimeoutID
};

BEGIN_EVENT_TABLE(MeterPanel, MeterPanelBase)
EVT_TIMER(OnMeterUpdateID, MeterPanel::OnMeterUpdate)
EVT_TIMER(OnTipTimeoutID, MeterPanel::OnTipTimeout)
EVT_SLIDER(wxID_ANY, MeterPanel::SetMixer)
EVT_MOUSE_EVENTS(MeterPanel::OnMouse)
EVT_CONTEXT_MENU(MeterPanel::OnContext)
EVT_KEY_DOWN(MeterPanel::OnKeyDown)
EVT_CHAR_HOOK(MeterPanel::OnCharHook)
EVT_SET_FOCUS(MeterPanel::OnSetFocus)
EVT_KILL_FOCUS(MeterPanel::OnKillFocus)
EVT_ERASE_BACKGROUND(MeterPanel::OnErase)
EVT_PAINT(MeterPanel::OnPaint)
EVT_SIZE(MeterPanel::OnSize)
EVT_MENU(OnMonitorID, MeterPanel::OnMonitor)
EVT_MENU(OnPreferencesID, MeterPanel::OnPreferences)
END_EVENT_TABLE()

IMPLEMENT_CLASS(MeterPanel, wxPanelWrapper)

MeterPanel::MeterPanel(AudacityProject* project,
                       wxWindow* parent, wxWindowID id,
                       bool isInput,
                       const wxPoint& pos /*= wxDefaultPosition*/,
                       const wxSize& size /*= wxDefaultSize*/,
                       Style style /*= HorizontalStereo*/,
                       float fDecayRate /*= 60.0f*/)
    : MeterPanelBase(parent, id, pos, size, wxTAB_TRAVERSAL | wxNO_BORDER | wxWANTS_CHARS),
    mProject(project),
    mQueue{1024},
    mWidth(size.x),
    mHeight(size.y),
    mIsInput(isInput),
    mDesiredStyle(style),
    mGradient(true),
    mDB(true),
    mDBRange(DecibelScaleCutoff.Read()),
    mDecay(true),
    mDecayRate(fDecayRate),
    mClip(true),
    mNumPeakSamplesToClip(3),
    mPeakHoldDuration(3),
    mT(0),
    mRate(0),
    mMonitoring(false),
    mActive(false),
    mNumBars(0),
    mLayoutValid(false),
    mBitmap{},
    mRuler{LinearUpdater::Instance(), LinearDBFormat::Instance()}
{
    // i18n-hint: Noun (the meter is used for playback or record level monitoring)
    SetName(XO("Meter"));
    // Suppress warnings about the header file
    wxUnusedVar(SpeakerMenu_xpm);
    wxUnusedVar(MicMenu_xpm);
    wxUnusedVar(PrefStyles);

    mStyle = mDesiredStyle;

    mIsFocused = false;

#if wxUSE_ACCESSIBILITY
    SetAccessible(safenew MeterAx(this));
#endif

    // Do this BEFORE UpdatePrefs()!
    mRuler.SetFonts(GetFont(), GetFont(), GetFont());
    mRuler.SetFlip(mStyle != MixerTrackCluster);
    mRuler.SetLabelEdges(true);
    //mRuler.SetTickColour( wxColour( 0,0,255 ) );

    if (mStyle != MixerTrackCluster) {
        mSlider = std::make_unique<LWSlider>(this, XO(""),
                                             pos,
                                             size,
                                             PERCENT_SLIDER,
                                             false, /* showlabels */
                                             false, /* drawticks */
                                             false, /* drawtrack */
                                             false /* alwayshidetip */
                                             );
        mSlider->SetScroll(0.1f, 2.0f);
    }

    UpdateSliderControl();
    UpdatePrefs();

    wxColour backgroundColour = theTheme.Colour(clrMedium);
    mBkgndBrush = wxBrush(backgroundColour, wxBRUSHSTYLE_SOLID);
    SetBackgroundColour(backgroundColour);

    mPeakPeakPen = wxPen(theTheme.Colour(clrMeterPeak),        1, wxPENSTYLE_SOLID);

    mAudioIOStatusSubscription = AudioIO::Get()
                                 ->Subscribe(*this, &MeterPanel::OnAudioIOStatus);

    mAudioCaptureSubscription = AudioIO::Get()
                                ->Subscribe(*this, &MeterPanel::OnAudioCapture);

    if (mIsInput) {
        mPen       = wxPen(theTheme.Colour(clrMeterInputPen), 1, wxPENSTYLE_SOLID);
        mBrush     = wxBrush(theTheme.Colour(clrMeterInputBrush), wxBRUSHSTYLE_SOLID);
        mRMSBrush  = wxBrush(theTheme.Colour(clrMeterInputRMSBrush), wxBRUSHSTYLE_SOLID);
        mClipBrush = wxBrush(theTheme.Colour(clrMeterInputClipBrush), wxBRUSHSTYLE_SOLID);
//      mLightPen  = wxPen(   theTheme.Colour( clrMeterInputLightPen    ), 1, wxSOLID);
//      mDarkPen   = wxPen(   theTheme.Colour( clrMeterInputDarkPen     ), 1, wxSOLID);
    } else {
        mPen       = wxPen(theTheme.Colour(clrMeterOutputPen), 1, wxPENSTYLE_SOLID);
        mBrush     = wxBrush(theTheme.Colour(clrMeterOutputBrush), wxBRUSHSTYLE_SOLID);
        mRMSBrush  = wxBrush(theTheme.Colour(clrMeterOutputRMSBrush), wxBRUSHSTYLE_SOLID);
        mClipBrush = wxBrush(theTheme.Colour(clrMeterOutputClipBrush), wxBRUSHSTYLE_SOLID);
//      mLightPen  = wxPen(   theTheme.Colour( clrMeterOutputLightPen   ), 1, wxSOLID);
//      mDarkPen   = wxPen(   theTheme.Colour( clrMeterOutputDarkPen    ), 1, wxSOLID);
    }

    mTipTimer.SetOwner(this, OnTipTimeoutID);
    mTimer.SetOwner(this, OnMeterUpdateID);
    // TODO: Yikes.  Hard coded sample rate.
    // JKC: I've looked at this, and it's benignish.  It just means that the meter
    // balistics are right for 44KHz and a bit more frisky than they should be
    // for higher sample rates.
    Reset(44100.0, true);
}

void MeterPanel::Clear()
{
    mQueue.Clear();
}

void MeterPanel::UpdatePrefs()
{
    mDBRange = DecibelScaleCutoff.Read();

    mMeterRefreshRate
        =std::max(MIN_REFRESH_RATE, std::min(MAX_REFRESH_RATE,
                                             gPrefs->Read(Key(wxT("RefreshRate")), 30L)));
    mGradient = gPrefs->Read(Key(wxT("Bars")), wxT("Gradient")) == wxT("Gradient");
    mDB = gPrefs->Read(Key(wxT("Type")), wxT("dB")) == wxT("dB");
    mMeterDisabled = gPrefs->Read(Key(wxT("Disabled")), 0L);

    if (mDesiredStyle != MixerTrackCluster) {
        wxString style = gPrefs->Read(Key(wxT("Style")));
        if (style == wxT("AutomaticStereo")) {
            mDesiredStyle = AutomaticStereo;
        } else if (style == wxT("HorizontalStereo")) {
            mDesiredStyle = HorizontalStereo;
        } else if (style == wxT("VerticalStereo")) {
            mDesiredStyle = VerticalStereo;
        } else {
            mDesiredStyle = AutomaticStereo;
        }
    }

    // Set the desired orientation (resets ruler orientation)
    SetActiveStyle(mDesiredStyle);

    // Reset to ensure NEW size is retrieved when language changes
    mLeftSize = wxSize(0, 0);
    mRightSize = wxSize(0, 0);

    Reset(mRate, false);

    mLayoutValid = false;

    Refresh(false);
}

static int MeterPrefsID()
{
    static int value = wxNewId();
    return value;
}

void MeterPanel::UpdateSelectedPrefs(int id)
{
    if (id == MeterPrefsID()) {
#if USE_PORTMIXER
        if (mIsInput && mSlider) {
            // Show or hide the input slider based on whether it works
            auto gAudioIO = AudioIO::Get();
            mSlider->SetEnabled(mEnabled && gAudioIO->InputMixerWorks());
        }
#endif
        UpdatePrefs();
    }
}

void MeterPanel::UpdateSliderControl()
{
#if USE_PORTMIXER
    float inputVolume;
    float playbackVolume;
    int inputSource;

    // Show or hide the input slider based on whether it works
    auto gAudioIO = AudioIO::Get();
    if (mIsInput && mSlider) {
        mSlider->SetEnabled(mEnabled && gAudioIO->InputMixerWorks());
    }

    gAudioIO->GetMixer(&inputSource, &inputVolume, &playbackVolume);

    const auto volume = mIsInput ? inputVolume : playbackVolume;

    if (mSlider && (mSlider->Get() != volume)) {
        mSlider->Set(volume);
    }

#endif // USE_PORTMIXER
}

void MeterPanel::OnErase(wxEraseEvent& WXUNUSED(event))
{
    // Ignore it to prevent flashing
}

void MeterPanel::OnPaint(wxPaintEvent& WXUNUSED(event))
{
#if defined(__WXMAC__)
    auto paintDC = std::make_unique<wxPaintDC>(this);
#else
    std::unique_ptr<wxDC> paintDC{ wxAutoBufferedPaintDCFactory(this) };
#endif
    wxDC& destDC = *paintDC;
    wxColour clrText = theTheme.Colour(clrTrackPanelText);
    wxColour clrBoxFill = theTheme.Colour(clrMedium);

    if (mLayoutValid == false || (mStyle == MixerTrackCluster)) {
        // Create a NEW one using current size and select into the DC
        mBitmap = std::make_unique<wxBitmap>();
        mBitmap->Create(mWidth, mHeight, destDC);
        wxMemoryDC dc;
        dc.SelectObject(*mBitmap);

        // Go calculate all of the layout metrics
        HandleLayout(dc);

        mBkgndBrush.SetColour(GetBackgroundColour());
        dc.SetPen(*wxTRANSPARENT_PEN);
        dc.SetBrush(mBkgndBrush);
        dc.DrawRectangle(0, 0, mWidth, mHeight);

        // MixerTrackCluster style has no icon or L/R labels
        if (mStyle != MixerTrackCluster) {
            dc.SetFont(GetFont());
            dc.SetTextForeground(clrText);
            dc.SetTextBackground(clrBoxFill);
            dc.DrawText(mLeftText, mLeftTextPos.x, mLeftTextPos.y);
            dc.DrawText(mRightText, mRightTextPos.x, mRightTextPos.y);
        }

        // Setup the colors for the 3 sections of the meter bars
        wxColor green(117, 215, 112);
        wxColor yellow(255, 255, 0);
        wxColor red(255, 0, 0);

        // Bug #2473 - (Sort of) Hack to make text on meters more
        // visible with darker backgrounds. It would be better to have
        // different colors entirely and as part of the theme.
        if (GetBackgroundColour().GetLuminance() < 0.25) {
            green = wxColor(117 - 100, 215 - 100, 112 - 100);
            yellow = wxColor(255 - 100, 255 - 100, 0);
            red = wxColor(255 - 100, 0, 0);
        } else if (GetBackgroundColour().GetLuminance() < 0.50) {
            green = wxColor(117 - 50, 215 - 50, 112 - 50);
            yellow = wxColor(255 - 50, 255 - 50, 0);
            red = wxColor(255 - 50, 0, 0);
        }

        // Draw the meter bars at maximum levels
        for (unsigned int i = 0; i < mNumBars; i++) {
            // Cache bar rect
            wxRect r = mBar[i].r;

            if (mGradient) {
                // Calculate the size of the two gradiant segments of the meter
                double gradw;
                double gradh;
                if (mDB) {
                    gradw = (double)r.GetWidth() / mDBRange * 6.0;
                    gradh = (double)r.GetHeight() / mDBRange * 6.0;
                } else {
                    gradw = (double)r.GetWidth() / 100 * 25;
                    gradh = (double)r.GetHeight() / 100 * 25;
                }

                if (mBar[i].vert) {
                    // Draw the "critical" segment (starts at top of meter and works down)
                    r.SetHeight(gradh);
                    dc.GradientFillLinear(r, red, yellow, wxSOUTH);

                    // Draw the "warning" segment
                    r.SetTop(r.GetBottom());
                    dc.GradientFillLinear(r, yellow, green, wxSOUTH);

                    // Draw the "safe" segment
                    r.SetTop(r.GetBottom());
                    r.SetBottom(mBar[i].r.GetBottom());
                    dc.SetPen(*wxTRANSPARENT_PEN);
                    dc.SetBrush(green);
                    dc.DrawRectangle(r);
                } else {
                    // Draw the "safe" segment
                    r.SetWidth(r.GetWidth() - (int)(gradw + gradw + 0.5));
                    dc.SetPen(*wxTRANSPARENT_PEN);
                    dc.SetBrush(green);
                    dc.DrawRectangle(r);

                    // Draw the "warning"  segment
                    r.SetLeft(r.GetRight() + 1);
                    r.SetWidth(floor(gradw));
                    dc.GradientFillLinear(r, green, yellow);

                    // Draw the "critical" segment
                    r.SetLeft(r.GetRight() + 1);
                    r.SetRight(mBar[i].r.GetRight());
                    dc.GradientFillLinear(r, yellow, red);
                }
            }
        }

        {//Strokes
            dc.SetPen(AColor::darkPen[0]);
            dc.SetBrush(*wxTRANSPARENT_BRUSH);

            //Stroke around group
            dc.DrawRectangle(wxRect(
                                 mBar[0].r.GetTopLeft(),
                                 mBar[mNumBars - 1].r.GetBottomRight()
                                 ).Inflate(1));

            //strokes between meters
            for (unsigned n = 0; n < mNumBars - 1; ++n) {
                dc.DrawLine(mBar[n].b.GetBottomLeft(), mBar[n].b.GetBottomRight());
            }
            if constexpr (gap > 0) {
                for (unsigned n = 1; n < mNumBars; ++n) {
                    dc.DrawLine(mBar[n].b.GetTopLeft(), mBar[n].b.GetTopRight());
                }
            }

            //stroke around clip indicators
            for (unsigned n = 0; n < mNumBars; ++n) {
                dc.DrawRectangle(wxRect(mBar[n].rClip).Inflate(1));
            }
        }

        mRuler.SetTickColour(clrText);
        dc.SetTextForeground(clrText);

        mRuler.Draw(dc); // Draw the ruler

        // Bitmap created...unselect
        dc.SelectObject(wxNullBitmap);
    }

    // Copy predrawn bitmap to the dest DC
    destDC.DrawBitmap(*mBitmap, 0, 0);

    // Go draw the meter bars, Left & Right channels using current levels
    for (unsigned int i = 0; i < mNumBars; i++) {
        DrawMeterBar(destDC, &mBar[i]);
    }

    destDC.SetTextForeground(clrText);

    // We can have numbers over the bars, in which case we have to draw them each time.
    if (mStyle == HorizontalStereoCompact || mStyle == VerticalStereoCompact) {
        mRuler.SetTickColour(clrText);
        // If the text colour is too similar to the meter colour, then we need a background
        // for the text.  We require a total of at least one full-scale RGB difference.
        int d = theTheme.ColourDistance(clrText, theTheme.Colour(clrMeterOutputRMSBrush));
        if (d < 256) {
            destDC.SetBackgroundMode(wxSOLID);
            destDC.SetTextBackground(clrBoxFill);
        }
        mRuler.Draw(destDC);
    }

    if (mStyle != MixerTrackCluster) {
        bool highlighted
            =wxRect{ mSliderPos, mSliderSize }.Contains(
            ScreenToClient(
                ::wxGetMousePosition()));

        mSlider->Move(mSliderPos);
        mSlider->AdjustSize(mSliderSize);
        mSlider->OnPaint(destDC, highlighted);
    }

    if (mIsFocused) {
        auto r = GetClientRect();
        AColor::DrawFocus(destDC, r);
    }
}

void MeterPanel::OnSize(wxSizeEvent& WXUNUSED(event))
{
    GetClientSize(&mWidth, &mHeight);

    mLayoutValid = false;
    Refresh();
}

void MeterPanel::OnMouse(wxMouseEvent& evt)
{
    if ((evt.GetEventType() == wxEVT_MOTION || evt.Entering() || evt.Leaving())) {
        mLayoutValid = false;
        Refresh();
    }

    if (mStyle == MixerTrackCluster) { // MixerTrackCluster style has no menu.
        return;
    }

    if (evt.Entering()) {
        mTipTimer.StartOnce(500);
    } else if (evt.Leaving()) {
        mTipTimer.Stop();
    }

    if (evt.RightDown()) {
        ShowMenu(evt.GetPosition());
    } else {
        if (mSlider) {
            mSlider->OnMouseEvent(evt);
        }
    }
}

void MeterPanel::OnCharHook(wxKeyEvent& evt)
{
    switch (evt.GetKeyCode()) {
    // These are handled in the OnCharHook handler because, on Windows at least, the
    // key up event will be passed on to the menu if we show it here.  This causes
    // the default sound to be heard if assigned.
    case WXK_RETURN:
    case WXK_NUMPAD_ENTER:
    case WXK_WINDOWS_MENU:
    case WXK_MENU:
        if (mStyle != MixerTrackCluster) {
            ShowMenu(GetClientRect().GetBottomLeft());
        } else {
            evt.Skip();
        }
        break;
    default:
        evt.Skip();
        break;
    }
}

void MeterPanel::OnContext(wxContextMenuEvent& evt)
{
    if (mStyle != MixerTrackCluster) { // MixerTrackCluster style has no menu.
        ShowMenu(GetClientRect().GetBottomLeft());
    } else {
        evt.Skip();
    }
}

void MeterPanel::OnKeyDown(wxKeyEvent& evt)
{
    switch (evt.GetKeyCode()) {
    case WXK_TAB:
        if (evt.ShiftDown()) {
            Navigate(wxNavigationKeyEvent::IsBackward);
        } else {
            Navigate(wxNavigationKeyEvent::IsForward);
        }
        break;
    default:
        mSlider->OnKeyDown(evt);
        break;
    }
}

void MeterPanel::OnSetFocus(wxFocusEvent& WXUNUSED(evt))
{
    mIsFocused = true;
    Refresh(false);
}

void MeterPanel::OnKillFocus(wxFocusEvent& WXUNUSED(evt))
{
    if (mSlider) {
        mSlider->OnKillFocus();
    }
    mTipTimer.Stop();

    mIsFocused = false;
    Refresh(false);
}

void MeterPanel::SetStyle(Style newStyle)
{
    if (mStyle != newStyle && mDesiredStyle == AutomaticStereo) {
        SetActiveStyle(newStyle);

        mLayoutValid = false;

        Refresh(false);
    }
}

void MeterPanel::SetMixer(wxCommandEvent& WXUNUSED(event))
{
#if USE_PORTMIXER
    if (mSlider) {
        float inputVolume;
        float outputVolume;
        int inputSource;

        Refresh();

        auto gAudioIO = AudioIO::Get();
        gAudioIO->GetMixer(&inputSource, &inputVolume, &outputVolume);

        if (mIsInput) {
            inputVolume = mSlider->Get();
        } else {
            outputVolume = mSlider->Get();
        }

        gAudioIO->SetMixer(inputSource, inputVolume, outputVolume);

#if wxUSE_ACCESSIBILITY
        GetAccessible()->NotifyEvent(wxACC_EVENT_OBJECT_VALUECHANGE,
                                     this,
                                     wxOBJID_CLIENT,
                                     wxACC_SELF);
#endif
    }
#endif // USE_PORTMIXER
}

bool MeterPanel::ShowDialog()
{
    if (!mSlider) {
        return false;
    }

    auto changed = mSlider->ShowDialog();
    if (changed) {
        wxCommandEvent e;
        SetMixer(e);
    }

    return changed;
}

void MeterPanel::Increase(float steps)
{
    if (mSlider) {
        wxCommandEvent e;

        mSlider->Increase(steps);
        SetMixer(e);
    }
}

void MeterPanel::Decrease(float steps)
{
    if (mSlider) {
        wxCommandEvent e;

        mSlider->Decrease(steps);
        SetMixer(e);
    }
}

void MeterPanel::Reset(double sampleRate, bool resetClipping)
{
    mT = 0;
    mRate = sampleRate;
    for (int j = 0; j < kMaxMeterBars; j++) {
        ResetBar(&mBar[j], resetClipping);
    }

    // wxTimers seem to be a little unreliable - sometimes they stop for
    // no good reason, so this "primes" it every now and then...
    mTimer.Stop();

    // While it's stopped, empty the queue
    mQueue.Clear();

    mLayoutValid = false;

    mTimer.Start(1000 / mMeterRefreshRate);

    Refresh(false);
}

static float floatMax(float a, float b)
{
    return a > b ? a : b;
}

/* Unused as yet.
static int intmin(int a, int b)
{
   return a<b? a: b;
}
*/

static int intmax(int a, int b)
{
    return a > b ? a : b;
}

static float ClipZeroToOne(float z)
{
    if (z > 1.0) {
        return 1.0;
    } else if (z < 0.0) {
        return 0.0;
    } else {
        return z;
    }
}

static float ToDB(float v, float range)
{
    double db;
    if (v > 0) {
        db = LINEAR_TO_DB(fabs(v));
    } else {
        db = -999;
    }
    return ClipZeroToOne((db + range) / range);
}

void MeterPanel::UpdateDisplay(
    unsigned numChannels, int numFrames, const float* sampleData)
{
    auto sptr = sampleData;
    auto num = std::min(numChannels, mNumBars);
    MeterUpdateMsg msg;

    memset(&msg, 0, sizeof(msg));
    msg.numFrames = numFrames;

    for (int i=0; i < numFrames; i++) {
        for (unsigned int j=0; j < num; j++) {
            msg.peak[j] = floatMax(msg.peak[j], fabs(sptr[j]));
            msg.rms[j] += sptr[j] * sptr[j];

            // In addition to looking for mNumPeakSamplesToClip peaked
            // samples in a row, also send the number of peaked samples
            // at the head and tail, in case there's a run of peaked samples
            // that crosses block boundaries
            if (fabs(sptr[j]) >= MAX_AUDIO) {
                if (msg.headPeakCount[j] == i) {
                    msg.headPeakCount[j]++;
                }
                msg.tailPeakCount[j]++;
                if (msg.tailPeakCount[j] > mNumPeakSamplesToClip) {
                    msg.clipping[j] = true;
                }
            } else {
                msg.tailPeakCount[j] = 0;
            }
        }
        sptr += numChannels;
    }
    for (unsigned int j=0; j < mNumBars; j++) {
        msg.rms[j] = sqrt(msg.rms[j] / numFrames);
    }

    mQueue.Put(msg);
}

// Vaughan, 2010-11-29: This not currently used. See comments in MixerTrackCluster::UpdateMeter().
//void MeterPanel::UpdateDisplay(int numChannels, int numFrames,
//                           // Need to make these double-indexed arrays if we handle more than 2 channels.
//                           float* maxLeft, float* rmsLeft,
//                           float* maxRight, float* rmsRight,
//                           const size_t kSampleCount)
//{
//   int i, j;
//   int num = intmin(numChannels, mNumBars);
//   MeterUpdateMsg msg;
//
//   msg.numFrames = kSampleCount;
//   for(j=0; j<mNumBars; j++) {
//      msg.peak[j] = 0.0;
//      msg.rms[j] = 0.0;
//      msg.clipping[j] = false;
//      msg.headPeakCount[j] = 0;
//      msg.tailPeakCount[j] = 0;
//   }
//
//   for(i=0; i<numFrames; i++) {
//      for(j=0; j<num; j++) {
//         msg.peak[j] = floatMax(msg.peak[j], ((j == 0) ? maxLeft[i] : maxRight[i]));
//         msg.rms[j] = floatMax(msg.rms[j], ((j == 0) ? rmsLeft[i] : rmsRight[i]));
//
//         // In addition to looking for mNumPeakSamplesToClip peaked
//         // samples in a row, also send the number of peaked samples
//         // at the head and tail, in case there's a run
//         // of peaked samples that crosses block boundaries.
//         if (fabs((j == 0) ? maxLeft[i] : maxRight[i]) >= MAX_AUDIO)
//         {
//            if (msg.headPeakCount[j]==i)
//               msg.headPeakCount[j]++;
//            msg.tailPeakCount[j]++;
//            if (msg.tailPeakCount[j] > mNumPeakSamplesToClip)
//               msg.clipping[j] = true;
//         }
//         else
//            msg.tailPeakCount[j] = 0;
//      }
//   }
//
//   mQueue.Put(msg);
//}

void MeterPanel::OnMeterUpdate(wxTimerEvent& WXUNUSED(event))
{
    MeterUpdateMsg msg;
    int numChanges = 0;

    // We shouldn't receive any events if the meter is disabled, but clear it to be safe
    if (mMeterDisabled) {
        mQueue.Clear();
        return;
    }

    // There may have been several update messages since the last
    // time we got to this function.  Catch up to real-time by
    // popping them off until there are none left.  It is necessary
    // to process all of them, otherwise we won't handle peaks and
    // peak-hold bars correctly.
    while (mQueue.Get(msg)) {
        numChanges++;
        double deltaT = msg.numFrames / mRate;

        mT += deltaT;
        for (unsigned int j=0; j < mNumBars; j++) {
            mBar[j].isclipping = false;

            //
            if (mDB) {
                msg.peak[j] = ToDB(msg.peak[j], mDBRange);
                msg.rms[j] = ToDB(msg.rms[j], mDBRange);
            }

            if (mDecay) {
                if (mDB) {
                    float decayAmount = mDecayRate * deltaT / mDBRange;
                    mBar[j].peak = floatMax(msg.peak[j],
                                            mBar[j].peak - decayAmount);
                } else {
                    double decayAmount = mDecayRate * deltaT;
                    double decayFactor = DB_TO_LINEAR(-decayAmount);
                    mBar[j].peak = floatMax(msg.peak[j],
                                            mBar[j].peak * decayFactor);
                }
            } else {
                mBar[j].peak = msg.peak[j];
            }

            // This smooths out the RMS signal
            float smooth = pow(0.9, (double)msg.numFrames / 1024.0);
            mBar[j].rms = mBar[j].rms * smooth + msg.rms[j] * (1.0 - smooth);

            if (mT - mBar[j].peakHoldTime > mPeakHoldDuration
                || mBar[j].peak > mBar[j].peakHold) {
                mBar[j].peakHold = mBar[j].peak;
                mBar[j].peakHoldTime = mT;
            }

            if (mBar[j].peak > mBar[j].peakPeakHold) {
                mBar[j].peakPeakHold = mBar[j].peak;
            }

            if (msg.clipping[j]
                || mBar[j].tailPeakCount + msg.headPeakCount[j]
                >= mNumPeakSamplesToClip) {
                mBar[j].clipping = true;
                mBar[j].isclipping = true;
            }

            mBar[j].tailPeakCount = msg.tailPeakCount[j];
        }
    } // while

    if (numChanges > 0) {
        RepaintBarsNow();
    }
}

void MeterPanel::OnTipTimeout(wxTimerEvent& evt)
{
    if (mSlider) {
        mSlider->ShowTip(true);
    }
}

float MeterPanel::GetMaxPeak() const
{
    float maxPeak = 0.;

    for (unsigned int j=0; j < mNumBars; j++) {
        maxPeak = mBar[j].peak > maxPeak ? mBar[j].peak : maxPeak;
    }

    return maxPeak;
}

float MeterPanel::GetPeakHold() const
{
    auto peakHold = .0f;
    for (unsigned int i = 0; i < mNumBars; i++) {
        peakHold = std::max(peakHold, mBar[i].peakPeakHold);
    }
    return peakHold;
}

wxFont MeterPanel::GetFont() const
{
    int fontSize = 10;
#if defined __WXMSW__
    fontSize = 8;
#endif

    return wxFont(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
}

void MeterPanel::ResetBar(MeterBar* b, bool resetClipping)
{
    b->peak = 0.0;
    b->rms = 0.0;
    b->peakHold = 0.0;
    b->peakHoldTime = 0.0;
    if (resetClipping) {
        b->clipping = false;
        b->peakPeakHold = 0.0;
    }
    b->isclipping = false;
    b->tailPeakCount = 0;
}

bool MeterPanel::IsActive() const
{
    return mActive;
}

bool MeterPanel::IsMonitoring() const
{
    return mMonitoring;
}

bool MeterPanel::IsClipping() const
{
    for (int c = 0; c < mNumBars; c++) {
        if (mBar[c].clipping) {
            return true;
        }
    }
    return false;
}

void MeterPanel::SetActiveStyle(Style newStyle)
{
    mStyle = newStyle;

    // Set dummy ruler bounds so width/height can be retrieved
    // NOTE: Make sure the Right and Bottom values are large enough to
    //       ensure full width/height of digits get calculated.
    mRuler.SetBounds(0, 0, 500, 500);

    if (mDB) {
        mRuler.SetFormat(&LinearDBFormat::Instance());
        if (mStyle == HorizontalStereo || mStyle == HorizontalStereoCompact) {
            mRuler.SetOrientation(wxHORIZONTAL);
            mRuler.SetRange(-mDBRange, 0);
        } else {
            mRuler.SetOrientation(wxVERTICAL);
            mRuler.SetRange(0, -mDBRange);
        }
    } else {
        mRuler.SetFormat(&RealFormat::LinearInstance());
        if (mStyle == HorizontalStereo || mStyle == HorizontalStereoCompact) {
            mRuler.SetOrientation(wxHORIZONTAL);
            mRuler.SetRange(0, 1);
        } else {
            mRuler.SetOrientation(wxVERTICAL);
            mRuler.SetRange(1, 0);
        }
    }

    mRuler.GetMaxSize(&mRulerWidth, &mRulerHeight);
}

void MeterPanel::SetBarAndClip(int iBar, bool vert)
{
    constexpr static auto StrokeWidth = 1;
    //either width or height, depending on meter orientation
    constexpr static auto ClipIndicatorSize = 3;
    constexpr static auto ClipIndicatorPadding = 1;
    // Save the orientation
    mBar[iBar].vert = vert;

    if (mClip) {
        // Create the clip rectangle and adjust
        // meter's boundaries
        if (vert) {
            mBar[iBar].rClip = wxRect(
                mBar[iBar].b.x + StrokeWidth,
                mBar[iBar].b.y + StrokeWidth,
                mBar[iBar].b.width - StrokeWidth * 2,
                ClipIndicatorSize
                );

            // Make room for the clipping indicator
            constexpr auto yOffset
                =gap + ClipIndicatorPadding + StrokeWidth * 2 + ClipIndicatorSize;
            mBar[iBar].b.y += yOffset;
            mBar[iBar].b.height -= yOffset;
        } else {
            mBar[iBar].rClip = wxRect(
                mBar[iBar].b.GetRight() - StrokeWidth - ClipIndicatorSize + 1,
                mBar[iBar].b.GetTop() + StrokeWidth,
                ClipIndicatorSize,
                mBar[iBar].b.height - StrokeWidth * 2
                );
            // Make room for the clipping indicator
            mBar[iBar].b.width
                -=gap + ClipIndicatorPadding + StrokeWidth * 2 + ClipIndicatorSize;
        }
    }
    // Create the bar rectangle and educe to fit inside the bevel
    mBar[iBar].r = mBar[iBar].b;
    mBar[iBar].r.Deflate(StrokeWidth);
}

void MeterPanel::HandleLayout(wxDC& dc)
{
    // Refresh to reflect any language changes
    /* i18n-hint: One-letter abbreviation for Left, in VU Meter */
    mLeftText = _("L");
    /* i18n-hint: One-letter abbreviation for Right, in VU Meter */
    mRightText = _("R");

    dc.SetFont(GetFont());
    int width = mWidth;
    int height = mHeight;
    int left = 0;
    int top = 0;
    int barw;
    int barh;
    int lside;
    int rside;

    // MixerTrackCluster has no L/R labels or icon
    if (mStyle != MixerTrackCluster) {
        if (mDesiredStyle == AutomaticStereo) {
            SetActiveStyle(width > height ? HorizontalStereo : VerticalStereo);
        }

        if (mStyle == HorizontalStereoCompact || mStyle == HorizontalStereo) {
            SetActiveStyle(height < 50 ? HorizontalStereoCompact : HorizontalStereo);
        } else if (mStyle == VerticalStereoCompact || mStyle == VerticalStereo) {
            SetActiveStyle(width < 100 ? VerticalStereoCompact : VerticalStereo);
        }

        if (mLeftSize.GetWidth() == 0) { // Not yet initialized to dc.
            dc.GetTextExtent(mLeftText, &mLeftSize.x, &mLeftSize.y);
            dc.GetTextExtent(mRightText, &mRightSize.x, &mRightSize.y);
        }
    }

    int ltxtWidth = mLeftSize.GetWidth();
    int ltxtHeight = mLeftSize.GetHeight();
    int rtxtWidth = mRightSize.GetWidth();
    int rtxtHeight = mRightSize.GetHeight();

    switch (mStyle) {
    default:
        wxPrintf(wxT("Style not handled yet!\n"));
        break;
    case MixerTrackCluster:
        // width is now the entire width of the meter canvas
        width -= mRulerWidth + left;

        // height is now the entire height of the meter canvas
        height -= top + gap;

        // barw is half of the canvas while allowing for a gap between meters
        barw = (width - gap) / 2;

        // barh is now the height of the canvas
        barh = height;

        // We always have 2 bars
        mNumBars = 2;

        // Save dimensions of the left bevel
        mBar[0].b = wxRect(left, top, barw, barh);

        // Save dimensions of the right bevel
        mBar[1].b = mBar[0].b;
        mBar[1].b.SetLeft(mBar[0].b.GetRight() + 1 + gap); // +1 for right edge

        // Set bar and clipping indicator dimensions
        SetBarAndClip(0, true);
        SetBarAndClip(1, true);

        mRuler.SetBounds(mBar[1].r.GetRight() + 1, // +1 for the bevel
                         mBar[1].r.GetTop(),
                         mWidth,
                         mBar[1].r.GetBottom());
        mRuler.OfflimitsPixels(0, 0);
        break;
    case VerticalStereo:
        // Determine required width of each side;
        lside = ltxtWidth + gap;
        rside = intmax(mRulerWidth, rtxtWidth);

        // left is now the right edge of the icon or L label
        left = lside;

        // Ensure there's a margin between top edge of window and the meters
        top = gap;

        // Position the L/R labels
        mLeftTextPos = wxPoint(left - ltxtWidth - gap, height - gap - ltxtHeight);
        mRightTextPos = wxPoint(width - rside - gap, height - gap - rtxtHeight);

        // left is now left edge of left bar
        left += gap;

        // width is now the entire width of the meter canvas
        width -= gap + rside + gap + left;

        // height is now the entire height of the meter canvas
        height -= top + gap;

        mSliderPos = wxPoint{ 0, top - gap };
        mSliderSize = wxSize{ width, height + 2 * gap };

        // barw is half of the canvas while allowing for a gap between meters
        barw = (width - gap) / 2;

        // barh is now the height of the canvas
        barh = height;

        // We always have 2 bars
        mNumBars = 2;

        // Save dimensions of the left bevel
        mBar[0].b = wxRect(left, top, barw, barh);

        // Save dimensions of the right bevel
        mBar[1].b = mBar[0].b;
        mBar[1].b.SetLeft(mBar[0].b.GetRight() + 1 + gap); // +1 for right edge

        // Set bar and clipping indicator dimensions
        SetBarAndClip(0, true);
        SetBarAndClip(1, true);

        mRuler.SetBounds(mBar[1].r.GetRight() + 1, // +1 for the bevel
                         mBar[1].r.GetTop(),
                         mWidth,
                         mBar[1].r.GetBottom());
        mRuler.OfflimitsPixels(mRightTextPos.y - gap, mBar[1].r.GetBottom());
        break;
    case VerticalStereoCompact:
        // Ensure there's a margin between top edge of window and the meters
        top = gap;

        // height is now the entire height of the meter canvas
        height -= top + gap + ltxtHeight + gap;

        mSliderPos = wxPoint{ 0, top - gap };
        mSliderSize = wxSize{ width, height + 2 * gap };

        // barw is half of the canvas while allowing for a gap between meters
        barw = (width / 2) - gap;

        // barh is now the height of the canvas
        barh = height;

        // We always have 2 bars
        mNumBars = 2;

        // Save dimensions of the left bevel
        mBar[0].b = wxRect(left, top, barw, barh);

        // Save dimensions of the right bevel
        mBar[1].b = mBar[0].b;
        mBar[1].b.SetLeft(mBar[0].b.GetRight() + 1 + gap); // +1 for right edge

        // Set bar and clipping indicator dimensions
        SetBarAndClip(0, true);
        SetBarAndClip(1, true);

        // L/R is centered horizontally under each bar
        mLeftTextPos = wxPoint(mBar[0].b.GetLeft() + ((mBar[0].b.GetWidth() - ltxtWidth) / 2), top + barh + gap);
        mRightTextPos = wxPoint(mBar[1].b.GetLeft() + ((mBar[1].b.GetWidth() - rtxtWidth) / 2), top + barh + gap);

        mRuler.SetBounds((mWidth - mRulerWidth) / 2,
                         mBar[1].r.GetTop(),
                         (mWidth - mRulerWidth) / 2,
                         mBar[1].r.GetBottom());
        mRuler.OfflimitsPixels(0, 0);
        break;
    case HorizontalStereo:
        // Button right next to dragger.
        left = 0;

        // Add a gap between bottom of icon and bottom of window
        height -= gap;

        left = gap;

        // Make sure there's room for icon and gap between the bottom of the meter and icon
        height -= rtxtHeight + gap;

        // L/R is centered vertically and to the left of a each bar
        mLeftTextPos = wxPoint(left, (height / 4) - ltxtHeight / 2);
        mRightTextPos = wxPoint(left, (height * 3 / 4) - rtxtHeight / 2);

        // Add width of widest of the L/R characters
        left += intmax(ltxtWidth, rtxtWidth); //, iconWidth);

        mSliderPos = wxPoint{ left - gap, 0 };

        // Add gap between L/R and meter bevel
        left += gap;

        // width is now the entire width of the meter canvas
        width -= left;

        mSliderSize = wxSize{ width + 2 * gap, height };

        // barw is now the width of the canvas minus gap between canvas and right window edge
        barw = width - gap;

        // barh is half of the canvas while allowing for a gap between meters
        barh = (height - gap) / 2;

        // We always have 2 bars
        mNumBars = 2;

        // Save dimensions of the top bevel
        mBar[0].b = wxRect(left, top, barw, barh);

        // Save dimensions of the bottom bevel
        mBar[1].b = mBar[0].b;
        mBar[1].b.SetTop(mBar[0].b.GetBottom() + 1 + gap); // +1 for bottom edge

        // Set bar and clipping indicator dimensions
        SetBarAndClip(0, false);
        SetBarAndClip(1, false);

        mRuler.SetBounds(mBar[1].r.GetLeft(),
                         mBar[1].r.GetBottom() + 1, // +1 to fit below bevel
                         mBar[1].r.GetRight(),
                         mHeight - mBar[1].r.GetBottom() + 1);
        break;
    case HorizontalStereoCompact:
        left = gap;

        // L/R is centered vertically and to the left of a each bar
        mLeftTextPos = wxPoint(left, (height / 4) - (ltxtHeight / 2));
        mRightTextPos = wxPoint(left, (height * 3 / 4) - (ltxtHeight / 2));

        // Add width of widest of the L/R characters
        left += intmax(ltxtWidth, rtxtWidth);

        mSliderPos = wxPoint{ left - gap, 0 };

        // Add gap between L/R and meter bevel
        left += gap;

        // width is now the entire width of the meter canvas
        width -= left;

        mSliderSize = wxSize{ width + 2 * gap, height };

        // barw is now the width of the canvas minus gap between canvas and window edge
        barw = width - gap;

        // barh is half of the canvas while allowing for a gap between meters
        barh = (height - gap) / 2;

        // We always have 2 bars
        mNumBars = 2;

        // Save dimensions of the top bevel
        mBar[0].b = wxRect(left, top, barw, barh);

        // Save dimensions of the bottom bevel
        // Since the bars butt up against the window's top and bottom edges, we need
        // to include an extra pixel in the bottom bar when the window height and
        // meter height do not exactly match.
        mBar[1].b = mBar[0].b;
        mBar[1].b.SetTop(mBar[0].b.GetBottom() + 1 + gap); // +1 for bottom bevel
        mBar[1].b.SetHeight(mHeight - mBar[1].b.GetTop() - 1); // +1 for bottom bevel

        // Add clipping indicators - do after setting bar/bevel dimensions above
        SetBarAndClip(0, false);
        SetBarAndClip(1, false);

        mRuler.SetBounds(mBar[1].r.GetLeft(),
                         mBar[1].b.GetTop() - (mRulerHeight / 2),
                         mBar[1].r.GetRight(),
                         mBar[1].b.GetTop() - (mRulerHeight / 2));
        mRuler.OfflimitsPixels(0, 0);
        break;
    }

    mLayoutValid = true;
}

void MeterPanel::RepaintBarsNow()
{
    if (mLayoutValid) {
        // Invalidate the bars so they get redrawn
        for (unsigned int i = 0; i < mNumBars; i++) {
            Refresh(false);
        }

        // Immediate redraw (using wxPaintDC)
        Update();

        return;
    }
}

void MeterPanel::DrawMeterBar(wxDC& dc, MeterBar* bar)
{
    // Cache some metrics
    wxCoord x = bar->r.GetLeft();
    wxCoord y = bar->r.GetTop();
    wxCoord w = bar->r.GetWidth();
    wxCoord h = bar->r.GetHeight();
    wxCoord ht;
    wxCoord wd;

    // Setup for erasing the background
    dc.SetPen(*wxTRANSPARENT_PEN);
    mMeterBkgndBrush.SetColour(theTheme.Colour(clrMeterBackground));
    dc.SetBrush(mMeterBkgndBrush);

    if (mGradient) {
        // Map the predrawn bitmap into the source DC
        wxMemoryDC srcDC;
        srcDC.SelectObject(*mBitmap);

        if (bar->vert) {
            // Copy as much of the predrawn meter bar as is required for the
            // current peak.
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            ht = (int)(bar->peak * (h - 1) + 0.5);

            // Blank out the rest
            if (h - ht) {
                // ht includes peak value...not really needed but doesn't hurt
                dc.DrawRectangle(x, y, w, h - ht);
            }

            // Copy as much of the predrawn meter bar as is required for the
            // current peak.
            // +/-1 to include the peak position
            if (ht) {
                dc.Blit(x, y + h - ht - 1, w, ht + 1, &srcDC, x, y + h - ht - 1);
            }

            // Draw the "recent" peak hold line using the predrawn meter bar so that
            // it will be the same color as the original level.
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            ht = (int)(bar->peakHold * (h - 1) + 0.5);
            if (ht > 1) {
                dc.Blit(x, y + h - ht - 1, w, 2, &srcDC, x, y + h - ht - 1);
            }

            // Draw the "maximum" peak hold line
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            dc.SetPen(mPeakPeakPen);
            ht = (int)(bar->peakPeakHold * (h - 1) + 0.5);
            if (ht > 0) {
                AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
                if (ht > 1) {
                    AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
                }
            }
        } else {
            // Calculate the peak position
            // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            wd = (int)(bar->peak * (w - 1) + 0.5);

            // Blank out the rest
            if (w - wd) {
                // wd includes peak value...not really needed but doesn't hurt
                dc.DrawRectangle(x + wd, y, w - wd, h);
            }

            // Copy as much of the predrawn meter bar as is required for the
            // current peak.  But, only blit() if there's something to copy
            // to prevent display corruption.
            // +1 to include peak position
            if (wd) {
                dc.Blit(x, y, wd + 1, h, &srcDC, x, y);
            }

            // Draw the "recent" peak hold line using the predrawn meter bar so that
            // it will be the same color as the original level.
            // -1 to give a 2 pixel width
            wd = (int)(bar->peakHold * (w - 1) + 0.5);
            if (wd > 1) {
                dc.Blit(x + wd - 1, y, 2, h, &srcDC, x + wd, y);
            }

            // Draw the "maximum" peak hold line using a themed color
            // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            dc.SetPen(mPeakPeakPen);
            wd = (int)(bar->peakPeakHold * (w - 1) + 0.5);
            if (wd > 0) {
                AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
                if (wd > 1) {
                    AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
                }
            }
        }

        // No longer need the source DC, so unselect the predrawn bitmap
        srcDC.SelectObject(wxNullBitmap);
    } else {
        if (bar->vert) {
            // Calculate the peak position
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            ht = (int)(bar->peak * (h - 1) + 0.5);

            // Blank out the rest
            if (h - ht) {
                // ht includes peak value...not really needed but doesn't hurt
                dc.DrawRectangle(x, y, w, h - ht);
            }

            // Draw the peak level
            // +/-1 to include the peak position
            dc.SetPen(*wxTRANSPARENT_PEN);
            dc.SetBrush(mBrush);
            if (ht) {
                dc.DrawRectangle(x, y + h - ht - 1, w, ht + 1);
            }

            // Draw the "recent" peak hold line
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            dc.SetPen(mPen);
            ht = (int)(bar->peakHold * (h - 1) + 0.5);
            if (ht > 0) {
                AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
                if (ht > 1) {
                    AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
                }
            }

            // Calculate the rms position
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            // +1 to include the rms position
            ht = (int)(bar->rms * (h - 1) + 0.5);

            // Draw the RMS level
            dc.SetPen(*wxTRANSPARENT_PEN);
            dc.SetBrush(mRMSBrush);
            if (ht) {
                dc.DrawRectangle(x, y + h - ht - 1, w, ht + 1);
            }

            // Draw the "maximum" peak hold line
            // (h - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            dc.SetPen(mPeakPeakPen);
            ht = (int)(bar->peakPeakHold * (h - 1) + 0.5);
            if (ht > 0) {
                AColor::Line(dc, x, y + h - ht - 1, x + w - 1, y + h - ht - 1);
                if (ht > 1) {
                    AColor::Line(dc, x, y + h - ht, x + w - 1, y + h - ht);
                }
            }
        } else {
            // Calculate the peak position
            // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            wd = (int)(bar->peak * (w - 1) + 0.5);

            // Blank out the rest
            if (w - wd) {
                // wd includes peak value...not really needed but doesn't hurt
                dc.DrawRectangle(x + wd, y, w - wd, h);
            }

            // Draw the peak level
            // +1 to include peak position
            dc.SetPen(*wxTRANSPARENT_PEN);
            dc.SetBrush(mBrush);
            if (wd) {
                dc.DrawRectangle(x, y, wd + 1, h);
            }

            // Draw the "recent" peak hold line
            // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            dc.SetPen(mPen);
            wd = (int)(bar->peakHold * (w - 1) + 0.5);
            if (wd > 0) {
                AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
                if (wd > 1) {
                    AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
                }
            }

            // Calculate the rms position
            // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            wd = (int)(bar->rms * (w - 1) + 0.5);

            // Draw the rms level
            // +1 to include the rms position
            dc.SetPen(*wxTRANSPARENT_PEN);
            dc.SetBrush(mRMSBrush);
            if (wd) {
                dc.DrawRectangle(x, y, wd + 1, h);
            }

            // Draw the "maximum" peak hold line using a themed color
            // (w - 1) corresponds to the mRuler.SetBounds() in HandleLayout()
            dc.SetPen(mPeakPeakPen);
            wd = (int)(bar->peakPeakHold * (w - 1) + 0.5);
            if (wd > 0) {
                AColor::Line(dc, x + wd, y, x + wd, y + h - 1);
                if (wd > 1) {
                    AColor::Line(dc, x + wd - 1, y, x + wd - 1, y + h - 1);
                }
            }
        }
    }

    // If meter had a clipping indicator, draw or erase it
    // LLL:  At least I assume that's what "mClip" is supposed to be for as
    //       it is always "true".
    if (mClip) {
        if (bar->clipping) {
            dc.SetBrush(mClipBrush);
        } else {
            dc.SetBrush(mBkgndBrush);
        }
        dc.SetPen(*wxTRANSPARENT_PEN);
        dc.DrawRectangle(bar->rClip);
    }
}

bool MeterPanel::IsMeterDisabled() const
{
    return mMeterDisabled != 0;
}

void MeterPanel::StartMonitoring()
{
    bool start = !mMonitoring;

    auto gAudioIO = AudioIO::Get();
    if (gAudioIO->IsMonitoring()) {
        gAudioIO->StopStream();
    }

    if (start && !gAudioIO->IsBusy()) {
        AudacityProject* p = mProject;
        if (p) {
            gAudioIO->StartMonitoring(ProjectAudioIO::GetDefaultOptions(*p));
        }

        mLayoutValid = false;

        Refresh(false);
    }
}

void MeterPanel::StopMonitoring()
{
    mMonitoring = false;
    auto gAudioIO = AudioIO::Get();
    if (gAudioIO->IsMonitoring()) {
        gAudioIO->StopStream();
    }
}

void MeterPanel::OnAudioIOStatus(AudioIOEvent evt)
{
    if (evt.type == AudioIOEvent::PAUSE) {
        return;
    }

    if (!mIsInput != (evt.type == AudioIOEvent::PLAYBACK)) {
        return;
    }

    AudacityProject* p = evt.pProject;
    mActive = evt.on && (p == mProject);
    if (mActive) {
        mTimer.Start(1000 / mMeterRefreshRate);
        if (evt.type == AudioIOEvent::MONITOR) {
            mMonitoring = mActive;
        }
    } else {
        mTimer.Stop();
        mMonitoring = false;
    }

    // Only refresh is we're the active meter
    if (IsShownOnScreen()) {
        Refresh(false);
    }
}

void MeterPanel::OnAudioCapture(AudioIOEvent event)
{
    if (
        event.type == AudioIOEvent::CAPTURE
        && (event.pProject != mProject || !event.on)) {
        mEnabled = !event.on;

        if (mSlider) {
            mSlider->SetEnabled(mEnabled);
        }
    }
}

// SaveState() and RestoreState() exist solely for purpose of recreating toolbars
// They should really be querying the project for current audio I/O state, but there
// isn't a clear way of doing that just yet.  (It should NOT query AudioIO.)
auto MeterPanel::SaveState() -> State
{
    return { true, mMonitoring, mActive };
}

void MeterPanel::RestoreState(const State& state)
{
    if (!state.mSaved) {
        return;
    }

    mMonitoring = state.mMonitoring;
    mActive = state.mActive;
    //wxLogDebug("Restore state for %p, is %i", this, mActive );

    if (mActive) {
        mTimer.Start(1000 / mMeterRefreshRate);
    }
}

//
// Pop-up menu
//

void MeterPanel::ShowMenu(const wxPoint& pos)
{
    wxMenu menu;
    // Note: these should be kept in the same order as the enum
    if (mIsInput) {
        wxMenuItem* mi;
        if (mMonitoring) {
            mi = menu.Append(OnMonitorID, _("Disable Silent Monitoring"));
        } else {
            mi = menu.Append(OnMonitorID, _("Enable Silent Monitoring"));
        }
        mi->Enable(!mActive || mMonitoring);
    }

    menu.Append(OnPreferencesID, _("Options..."));

    BasicMenu::Handle{ &menu }.Popup(
        wxWidgetsWindowPlacement { this },
        { pos.x, pos.y }
        );
}

void MeterPanel::SetName(const TranslatableString& tip)
{
    wxPanelWrapper::SetName(tip);
    if (mSlider) {
        mSlider->SetName(tip);
    }
}

void MeterPanel::OnMonitor(wxCommandEvent& WXUNUSED(event))
{
    StartMonitoring();
}

void MeterPanel::OnPreferences(wxCommandEvent& WXUNUSED(event))
{
    wxTextCtrl* rate;
    wxRadioButton* gradient;
    wxRadioButton* rms;
    wxRadioButton* db;
    wxRadioButton* linear;
    wxRadioButton* automatic;
    wxRadioButton* horizontal;
    wxRadioButton* vertical;
    int meterRefreshRate = mMeterRefreshRate;

    auto title = mIsInput ? XO("Recording Meter Options") : XO("Playback Meter Options");

    // Dialog is a child of the project, rather than of the toolbar.
    // This determines where it pops up.

    wxDialogWrapper dlg(FindProjectFrame(mProject), wxID_ANY, title);
    dlg.SetName();
    ShuttleGui S(&dlg, eIsCreating);
    S.StartVerticalLay();
    {
        S.StartStatic(XO("Refresh Rate"), 0);
        {
            S.AddFixedText(XO(
                               "Higher refresh rates make the meter show more frequent\nchanges. A rate of 30 per second or less should prevent\nthe meter affecting audio quality on slower machines."));
            S.StartHorizontalLay();
            {
                rate = S.Name(XO("Meter refresh rate per second [1-100]"))
                       .Validator<IntegerValidator<long> >(
                    &mMeterRefreshRate, NumValidatorStyle::DEFAULT,
                    MIN_REFRESH_RATE, MAX_REFRESH_RATE)
                       .AddTextBox(XXO("Meter refresh rate per second [1-100]: "),
                                   wxString::Format(wxT("%d"), meterRefreshRate),
                                   10);
            }
            S.EndHorizontalLay();
        }
        S.EndStatic();

        S.StartHorizontalLay();
        {
            S.StartStatic(XO("Meter Style"), 0);
            {
                S.StartVerticalLay();
                {
                    gradient = S.AddRadioButton(XXO("Gradient"), true, mGradient);
                    rms = S.AddRadioButtonToGroup(XXO("RMS"), false, mGradient);
                }
                S.EndVerticalLay();
            }
            S.EndStatic();

            S.StartStatic(XO("Meter Type"), 0);
            {
                S.StartVerticalLay();
                {
                    db = S.AddRadioButton(XXO("dB"), true, mDB);
                    linear = S.AddRadioButtonToGroup(XXO("Linear"), false, mDB);
                }
                S.EndVerticalLay();
            }
            S.EndStatic();

            S.StartStatic(XO("Orientation"), 1);
            {
                S.StartVerticalLay();
                {
                    automatic = S.AddRadioButton(
                        XXO("Automatic"), AutomaticStereo, mDesiredStyle);
                    horizontal = S.AddRadioButtonToGroup(
                        XXO("Horizontal"), HorizontalStereo, mDesiredStyle);
                    vertical = S.AddRadioButtonToGroup(
                        XXO("Vertical"), VerticalStereo, mDesiredStyle);
                }
                S.EndVerticalLay();
            }
            S.EndStatic();
        }
        S.EndHorizontalLay();
        S.AddStandardButtons();
    }
    S.EndVerticalLay();
    dlg.Layout();
    dlg.Fit();

    dlg.CenterOnParent();

    if (dlg.ShowModal() == wxID_OK) {
        wxArrayStringEx style{
            wxT("AutomaticStereo"),
            wxT("HorizontalStereo"),
            wxT("VerticalStereo"),
        };

        int s = 0;
        s = automatic->GetValue() ? 0 : s;
        s = horizontal->GetValue() ? 1 : s;
        s = vertical->GetValue() ? 2 : s;

        gPrefs->Write(Key(wxT("Style")), style[s]);
        gPrefs->Write(Key(wxT("Bars")), gradient->GetValue() ? wxT("Gradient") : wxT("RMS"));
        gPrefs->Write(Key(wxT("Type")), db->GetValue() ? wxT("dB") : wxT("Linear"));
        gPrefs->Write(Key(wxT("RefreshRate")), rate->GetValue());

        gPrefs->Flush();

        // Currently, there are 2 playback meters and 2 record meters and any number of
        // mixerboard meters, so we have to send out an preferences updated message to
        // ensure they all update themselves.
        PrefsListener::Broadcast(MeterPrefsID());
    }
}

wxString MeterPanel::Key(const wxString& key) const
{
    if (mStyle == MixerTrackCluster) {
        return wxT("/Meter/Mixerboard/") + key;
    }

    if (mIsInput) {
        return wxT("/Meter/Input/") + key;
    }

    return wxT("/Meter/Output/") + key;
}

// This compensates for a but in wxWidgets 3.0.2 for mac:
// Couldn't set focus from keyboard when AcceptsFocus returns false;
// this bypasses that limitation
void MeterPanel::SetFocusFromKbd()
{
    auto temp = TemporarilyAllowFocus();
    SetFocus();
}

#if wxUSE_ACCESSIBILITY

MeterAx::MeterAx(wxWindow* window)
    : WindowAccessible(window)
{
}

MeterAx::~MeterAx()
{
}

// Performs the default action. childId is 0 (the action for this object)
// or > 0 (the action for a child).
// Return wxACC_NOT_SUPPORTED if there is no default action for this
// window (e.g. an edit control).
wxAccStatus MeterAx::DoDefaultAction(int WXUNUSED(childId))
{
    MeterPanel* m = wxDynamicCast(GetWindow(), MeterPanel);

    if (m && m->mIsInput) {
        m->StartMonitoring();
    }

    return wxACC_OK;
}

// Retrieves the address of an IDispatch interface for the specified child.
// All objects must support this property.
wxAccStatus MeterAx::GetChild(int childId, wxAccessible** child)
{
    if (childId == wxACC_SELF) {
        *child = this;
    } else {
        *child = NULL;
    }
    return wxACC_OK;
}

// Gets the number of children.
wxAccStatus MeterAx::GetChildCount(int* childCount)
{
    *childCount = 0;
    return wxACC_OK;
}

// Gets the default action for this object (0) or > 0 (the action for
// a child).  Return wxACC_OK even if there is no action. actionName
// is the action, or the empty string if there is no action.  The
// retrieved string describes the action that is performed on an
// object, not what the object does as a result. For example, a
// toolbar button that prints a document has a default action of
// "Press" rather than "Prints the current document."
wxAccStatus MeterAx::GetDefaultAction(int WXUNUSED(childId), wxString* actionName)
{
    *actionName = _("Press");
    return wxACC_OK;
}

// Returns the description for this object or a child.
wxAccStatus MeterAx::GetDescription(int WXUNUSED(childId), wxString* description)
{
    description->clear();
    return wxACC_NOT_SUPPORTED;
}

// Gets the window with the keyboard focus.
// If childId is 0 and child is NULL, no object in
// this subhierarchy has the focus.
// If this object has the focus, child should be 'this'.
wxAccStatus MeterAx::GetFocus(int* childId, wxAccessible** child)
{
    *childId = 0;
    *child = this;
    return wxACC_OK;
}

// Returns help text for this object or a child, similar to tooltip text.
wxAccStatus MeterAx::GetHelpText(int WXUNUSED(childId), wxString* helpText)
{
    helpText->clear();
    return wxACC_NOT_SUPPORTED;
}

// Returns the keyboard shortcut for this object or child.
// Return e.g. ALT+K
wxAccStatus MeterAx::GetKeyboardShortcut(int WXUNUSED(childId), wxString* shortcut)
{
    shortcut->clear();
    return wxACC_OK;
}

// Returns the rectangle for this object (id = 0) or a child element (id > 0).
// rect is in screen coordinates.
wxAccStatus MeterAx::GetLocation(wxRect& rect, int WXUNUSED(elementId))
{
    MeterPanel* m = wxDynamicCast(GetWindow(), MeterPanel);

    rect = m->GetClientRect();
    rect.SetPosition(m->ClientToScreen(rect.GetPosition()));

    return wxACC_OK;
}

// Returns a role constant.
wxAccStatus MeterAx::GetRole(int WXUNUSED(childId), wxAccRole* role)
{
    *role = wxROLE_SYSTEM_SLIDER;

    return wxACC_OK;
}

// Gets a variant representing the selected children
// of this object.
// Acceptable values:
// - a null variant (IsNull() returns TRUE)
// - a list variant (GetType() == wxT("list"))
// - an integer representing the selected child element,
//   or 0 if this object is selected (GetType() == wxT("long"))
// - a "void*" pointer to a wxAccessible child object
wxAccStatus MeterAx::GetSelections(wxVariant* WXUNUSED(selections))
{
    return wxACC_NOT_IMPLEMENTED;
}

// Returns a state constant.
wxAccStatus MeterAx::GetState(int WXUNUSED(childId), long* state)
{
    MeterPanel* m = wxDynamicCast(GetWindow(), MeterPanel);

    *state = wxACC_STATE_SYSTEM_FOCUSABLE;
    *state |= (m == wxWindow::FindFocus() ? wxACC_STATE_SYSTEM_FOCUSED : 0);

    return wxACC_OK;
}

// Returns a localized string representing the value for the object
// or child.
wxAccStatus MeterAx::GetValue(int WXUNUSED(childId), wxString* strValue)
{
    MeterPanel* m = wxDynamicCast(GetWindow(), MeterPanel);

    *strValue = m->mSlider->GetStringValue();
    return wxACC_OK;
}

#endif
